﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Chambills.Present.Core;

namespace Chambills.Present.Validation
{
    /// <summary>
    /// Uses Annotation based Validations on a ViewMOdel
    /// </summary>
    public class AttributeBasedViewModelValidatorContributor : IViewModelValidationContributor
    {

        private Type modelType;
        private IViewModel model;

        public IViewModel Model
        {
            get { return model; }
            set
            {
                model = value;
                modelType = value == null ? typeof(object) : value.GetType();
            }
        }

        protected Dictionary<string, ValidationAttribute[]> Validators
        {
            get { return ReflectionCache.GetFor(modelType).Validators; }
        }

        protected Dictionary<string, Func<IViewModel, object>> PropertyGetters
        {
            get { return ReflectionCache.GetFor(modelType).PropertyGetters; }
        }


        public List<string> GetErrorsForProperty(string name)
        {
            if (PropertyGetters.ContainsKey(name))
            {
                var value = PropertyGetters[name](Model);
                return Validators[name]
                    .AsParallel()
                    .Where(v => !v.IsValid(value))
                    .Select(v => v.ErrorMessage).ToList();
            }

            return new List<string>();
        }

        public List<string> GetCurrentErrors()
        {
            var errors = from i in Validators.AsParallel()
                         from v in i.Value
                         where !v.IsValid(PropertyGetters[i.Key](Model))
                         select v.ErrorMessage;

            return errors.ToList();
        }



        /// <summary>
        /// Caches the results of reflection calls
        /// </summary>
        internal class ReflectionCache
        {
            private readonly Type viewModelType;
            private PropertyInfo[] properties;

            public ReflectionCache(Type viewModelType)
            {
                this.viewModelType = viewModelType;
                properties = GetDistinctProperties();
                PropertyGetters = GetAllPropertyGetters();
                Validators = GetValidatorsPerProperty();
            }

            public Dictionary<string, Func<IViewModel, object>> PropertyGetters { get; private set; }
            public Dictionary<string, ValidationAttribute[]> Validators { get; private set; }



            private Dictionary<string, Func<IViewModel, object>> GetAllPropertyGetters()
            {
                return viewModelType.GetProperties()
                    .AsParallel()
                    .Where(p => GetValidations(p).Length != 0)
                    .ToDictionary(p => p.Name, GetValueGetter);
            }


            private Dictionary<string, ValidationAttribute[]> GetValidatorsPerProperty()
            {
                return viewModelType.GetProperties()
                    .AsParallel()
                    .Where(p => GetValidations(p).Length != 0)
                    .ToDictionary(p => p.Name, GetValidations);
            }

            private PropertyInfo[] GetDistinctProperties()
            {
                return viewModelType.GetProperties().GroupBy(p => p.Name).Select(p => p.First()).ToArray();
            }



            private ValidationAttribute[] GetValidations(PropertyInfo property)
            {
                return
                    (ValidationAttribute[])Attribute.GetCustomAttributes(property, typeof(ValidationAttribute), false);
            }

            private Func<IViewModel, object> GetValueGetter(PropertyInfo property)
            {
                var instance = Expression.Parameter(typeof(IViewModel), "i");
                var typedInstance = Expression.TypeAs(instance, viewModelType);
                var propertyCastAsObject = Expression.TypeAs(Expression.Property(typedInstance, property), typeof(object));
                var valueGetterLambda = Expression.Lambda(propertyCastAsObject, instance);

                return (Func<IViewModel, object>)valueGetterLambda.Compile();
            }


            static readonly Dictionary<Type, ReflectionCache> cache = new Dictionary<Type, ReflectionCache>();
            public static ReflectionCache GetFor(Type type)
            {
                lock (cache)
                {
                    if (!cache.ContainsKey(type))
                    {
                        cache.Add(type, new ReflectionCache(type));
                    }

                    return cache[type];
                }
            }
        }



       
    }
}